LiveView automatically synchronizes state between the client and server, ensuring a smooth user experience even with network latency.
The Challenge
In any web application, client and server state can diverge temporarily due to network latency. Consider this scenario:
Step 1: User types "hello@example." and debounce triggers:
[ hello@example. ]
------------
SUBMIT
------------
Step 2: User finishes typing and clicks submit:
[ hello@example.com ]
------------
SUBMITTING
------------
Step 3: Server response from debounce arrives:
Without proper handling, the client would roll back to:
[ hello@example. ] ✓
------------
SUBMIT
------------
This rollback would be disastrous—the user’s completed input and button state would be lost!
LiveView’s Solution
LiveView automatically handles this synchronization:
Client is source of truth for current input values
Tracks in-flight events and only applies changes once all events resolve
Preserves focus state across server updates
For most cases, LiveView handles client-server synchronization automatically without any code changes needed.
See Form Bindings - JavaScript Client Specifics for complete details.
Optimistic UI with Loading Classes
LiveView automatically adds CSS loading classes to elements pushing events to the server.
Automatic Loading Classes
<button phx-click="clicked" phx-window-keydown="key">Click me</button>
On click: receives phx-click-loading class
On keydown: receives phx-keydown-loading class
Classes removed when server acknowledges the event
Available Loading Classes
Event Loading Class phx-clickphx-click-loadingphx-changephx-change-loadingphx-submitphx-submit-loadingphx-focusphx-focus-loadingphx-blurphx-blur-loadingphx-window-keydownphx-keydown-loadingphx-window-keyupphx-keyup-loading
CSS Loading States
Create immediate visual feedback with CSS:
.phx-click-loading.opaque-on-click {
opacity : 50 % ;
}
<button phx-click="save" class="opaque-on-click">Save</button>
For events inside forms, loading classes apply to both the element and the form:
<form id="user-form" phx-change="validate" phx-submit="save">
<input name="email" />
<button type="submit">Save</button>
</form>
Input change: phx-change-loading on both input and form
Form submit: phx-submit-loading on both button and form
Disabled State
Customize button text during loading:
<button phx-disable-with="Submitting..." type="submit">Submit</button>
Buttons are automatically disabled during server acknowledgement. Use phx-disable-with to customize the text.
Tailwind Integration
Add custom variants for cleaner loading states:
// tailwind.config.js
plugins : [
plugin (({ addVariant }) => {
addVariant ( "phx-click-loading" , [
".phx-click-loading&" ,
".phx-click-loading &"
])
addVariant ( "phx-submit-loading" , [
".phx-submit-loading&" ,
".phx-submit-loading &"
])
addVariant ( "phx-change-loading" , [
".phx-change-loading&" ,
".phx-change-loading &"
])
})
]
Use in templates:
<button phx-click="clicked" class="phx-click-loading:opacity-50">
Click me
</button>
<div class="phx-submit-loading:animate-pulse">
<button type="submit">Save</button>
</div>
Optimistic UI with JS Commands
For more control, use JS commands to specify which elements get loading states:
<button phx-click={JS.push("delete", loading: "#post-row-13")}>
Delete
</button>
Common Patterns
JS commands are DOM-patch aware—operations persist across server updates.
Custom Client Events
Dispatch custom DOM events for specialized interactions:
Event Listener
Trigger Event
window . addEventListener ( "app:clipcopy" , ( event ) => {
if ( "clipboard" in navigator ) {
if ( event . target . tagName === "INPUT" ) {
navigator . clipboard . writeText ( event . target . value )
} else {
navigator . clipboard . writeText ( event . target . textContent )
}
} else {
alert (
"Sorry, your browser does not support clipboard copy."
)
}
})
Advanced: JS Hooks
For complex cases, use hooks to control exactly when server updates apply:
Hooks . CustomInput = {
mounted () {
// Take control of element updates
},
beforeUpdate () {
// Prepare for server update
},
updated () {
// Selectively apply server changes
}
}
See Client Hooks for detailed documentation.
Live Navigation
LiveView provides classes and events for navigation state:
Connection Classes
Applied to the LiveView’s parent container:
Class Description phx-connectedView connected to server phx-loadingView not connected to server phx-errorError occurred (applied with phx-loading)
Page Loading Events
Triggered for navigation via <.link>, push_navigate, push_patch, and phx-submit:
import topbar from "topbar"
window . addEventListener ( "phx:page-loading-start" , info => {
topbar . show ( 500 )
console . log ( "Navigation kind:" , info . detail . kind )
})
window . addEventListener ( "phx:page-loading-stop" , info => {
topbar . hide ()
})
Navigation Event
Triggered whenever the URL changes:
window . addEventListener ( "phx:navigate" , ( e ) => {
console . log ( "Navigated to:" , e . detail . href )
console . log ( "Is patch:" , e . detail . patch )
console . log ( "Is back/forward:" , e . detail . pop )
})
For navigation-aware logic, prefer phx:navigate over hook updated() callbacks, as hooks may fire before window.location is updated.
Complete Example: Optimistic Delete
<div id={"post-#{@post.id}"} class="post-row">
<h3>{@post.title}</h3>
<button phx-click={
JS.push("delete", value: %{id: @post.id})
|> JS.hide(to: "#post-#{@post.id}", transition: "fade-out-scale")
} class="btn-delete">
Delete
</button>
</div>
Best Practices
Start with CSS loading classes for simple feedback: .btn.phx-click-loading {
opacity : 0.6 ;
cursor : wait ;
}
Use JS commands when loading state affects multiple elements: <button phx-click={JS.push("save", loading: ".form-container")}>
Save
</button>
Reserve hooks for cases needing complete DOM control: Hooks . ComplexWidget = {
beforeUpdate () {
// Save widget state
},
updated () {
// Restore widget state selectively
}
}
Always test with simulated latency: liveSocket . enableLatencySim ( 1000 )
See Also